# h1=. gui.py

# p=. *Nifty GUI Widgets* 
# _jfuller 2009-05-21_

# This module implements several convenience functions and helper
# classes that are useful when building a wxPython based GUI.

# h2. Front Matter

# h3. Imports

import sys, os, cStringIO, math, stat, re, pprint, wx
import wx.lib.buttons as wbuttons
import buttons, paths
import wx.lib.mixins.listctrl as listmix

# h3. Settings

# Various functions expect to find theme information in a globally
# available settings module (a la shaytan). Since I'm not using a
# settings file here, I fake it with this data structure:

class section:
    def __init__(my, __doc__=None, **kwargs):
        my.__doc__ = __doc__
        [setattr(my, x, kwargs[x]) for x in kwargs]

settings = section(
    program = section(
        theme = section(
            bg        = "#404040",
            fg        = "#2a2215",
            field_bg  = "#0e131c",
            field_fg  = "#a0a0a0",
            highlight = "#959595",
            )))

# h3. Event Setup

myEVT_INVALID = wx.NewEventType()
EVT_INVALID = wx.PyEventBinder(myEVT_INVALID, 1)

# h2. Predicates

def intp(data): return isinstance(data, int)

def stringp(data): # I hate unicode...
    return isinstance(data, basestring)

def tuplep(data): return isinstance(data, tuple)

def listp(data): return isinstance(data, list)

def hashp(data):
    return isinstance(data, dict)

# h2. Convenience Classes

# As per "Norvig:":http://norvig.com/python-iaq.html

class Struct:
    def __init__(self, **entries): self.__dict__.update(entries)

# Python no longer allows string exceptions, so I need to have an
# specific exception object available to signal anything not covered
# by the built-ins.

class error(Exception):
    def __init__(my, message): my._message = message
    def __str__(my): return my._message
    def message(my): return my._message

# h2. Convenience Functions

# h3. Colour Manipulation

def colerp(a, b, d):
    "( a b d -- c) Linear interpolation of colour a to b. d is distance (0 to 1)."

    if a == b: return a # If same, why lerp?

    at = a.Get(); bt = b.Get()
    m = 1.0 - d
    # I tried the a.Red()... functions, but they have bogus values.
    return wx.Colour((a[0] * m) + (b[0] * d),
                     (a[1] * m) + (b[1] * d),
                     (a[2] * m) + (b[2] * d))

def columa(c):
    "( c -- y) Convert colour to luminance for contrast test."

    ct = c.Get()
    return (ct[0] * .3) + (ct[1] * .59) + (ct[2] * .11)


def unhex(colour):
    "( colour -- (red, green, blue)) Convert hex colour spec to tuple."

    return apply(wx.Colour, [
            int(i, 16) for i in re.search(
                "([0-9A-Fa-f][0-9A-Fa-f])"  # Red.
                "([0-9A-Fa-f][0-9A-Fa-f])"  # Green.
                "([0-9A-Fa-f][0-9A-Fa-f])", # Blue.
                colour).groups()])


def recolour(widget, theme):
    "( widget theme --) Recursive function to change window/subwindow colours."
    
    x = theme
    if callable(theme): x = theme() # Allows for dynamic colour changes...

    try: widget.theme(x)
    except: pass # Can't change colours for this widget.

    for kid in widget.Children: recolour(kid, theme)


# h3. Function visible @( window -- flag)@

# Determines if the specified window's _origin_ is visible on
# screen. A more proper implementation of this would actually check to
# make sure the entire window surface was shown, but since sometimes
# users like to push the window off the edge a bit, using the origin
# allows this (at least for the bottom and right edge).

# Here, I walk through all the displays connected to the machine and
# make sure the window origin is available on at least one of them.

# By the way, if you thought the wxPython @IsShownOnScreen()@ method
# would deal with this, you would be wrong.

def visible(w):
    ok = False
    d = wx.Display()

    for i in range(d.GetCount()):
        if wx.Display(i).GetClientArea().Contains(
            w.GetPosition()): ok = True

    return ok

# h3. GUI Functions

# It is often desirable to break up a long list into
# sections. However, the list might be have a lot of items that start
# with the same letters, making it hard to determine a good breaking
# point. Here I'll take a list along with a desired number of sections
# and chop it up as needed. 

# I return a list containing the items that need to have a break.

def breaklist(items, sections=1, level=0):
    "( [item,] /sections/ -- {item: True,}) Break list into sections."

    breaks = []

    shortest = 10
    last = None
    for item in items:
        if last and last[:level] != item[:level]: 
            breaks.append(last)
        last = item
        shortest = min(shortest, len(item))

    if len(breaks) >= sections or level >= shortest: return breaks
    return breaklist(items, sections, level + 1)

def setto(name, widget, value):
    "( name widget value -- widget) Set selected in widget to value."

    items = widget.GetItems()
    if value not in items:
        raise error("Item not in %s: %s" % (name, value))
    widget.SetSelection(items.index(value))

    return widget

def menufy(appframe, menus):
    "( appframe menus -- menubar) Create menu for app from definition."

    accelerators = []
    mb = wx.MenuBar()
    mb.SetBackgroundColour(settings.program.theme.bg)
    mb.SetForegroundColour(settings.program.theme.fg)
    appframe.SetMenuBar(mb)

    for menu in menus:
        m = wx.Menu()
        mb.Append(m, menu[0])
        for i in menu[1:]:
            if "--" == i: m.AppendSeparator(); continue
            i = list(i) + [None, None, None, wx.NewId()][len(i):]
            (iname, ido, ikey, iwhich) = i

            if iname.startswith("^"): 
                iname = iname[1:]
                im = m.AppendCheckItem(iwhich, iname)
            else: im = m.Append(iwhich, iname)

            if ikey: accelerators.append((ikey[0], ikey[1], iwhich))
            if ido: appframe.Bind(wx.EVT_MENU, ido, im)
            else: im.Enable(False)
    appframe.accelerators = accelerators
    appframe.SetAcceleratorTable(wx.AcceleratorTable(appframe.accelerators))

    return mb

# h2. Themed Widgets

# wxPython (or wxWindows) doesn't feature a generic theming
# capability. These replacement gadgets attempt to provide it with the
# @theme()@ method. In my opinion, they also look better...

# h3. class sizerpanel

# It's very confusing to deal with panels with separate sizers. This
# class attempts to make things easier to keep track of by integrating
# a simple box sizer with the panel.

class sizerpanel(wx.Panel):
    def __init__(my, parent, id=-1, pos=wx.DefaultPosition,
                 size=wx.DefaultSize, orient=wx.VERTICAL, widgets=None,
                 **kwargs):
        apply(wx.Panel.__init__, (my, parent, id, pos, size), kwargs)
        sizer = wx.BoxSizer(orient)
        my.SetSizer(sizer)

        # XXX This may be needed for backward compatibility only. jfuller 2011-03-30
        my._widgets = Struct()

        if widgets: my.populate(widgets)

        my.theme({"bg": settings.program.theme.bg,
                  "fg": settings.program.theme.fg})

    def Add(my, *args, **kwargs): 
        s = my.GetSizer()
        apply(s.Add, args, kwargs)

    def AddF(my, *args, **kwargs):
        s = my.GetSizer()
        apply(s.AddF, args, kwargs)

    def AddMany(my, items): 
        my.GetSizer().AddMany(items)

# The @populate()@ function builds a GUI based on a list of
# directives. It handles several types of directives:

# |_. Directive |_. Description |_. Interepretation | 
# | @"Text"@ | String | Creates default gui.text object with this value. | 
# | @(5, 5)@ | Size Tuple | Creates spacer of defined size. | 
# | @(gui.widget, arg, ...)@ | Form Tuple | Executes first item in form with remaining items as arguments. | 

# If a directive is a tuple and starts with a @wx.SizerFlags@
# instance, then the sizer flags are applied to all succeding
# elements.

    def populate(my, widgets):
        for w in widgets:
            f = wx.SizerFlags().Centre()
            if tuplep(w) and isinstance(w[0], wx.SizerFlags):
                f, w = w[0], w[1:]
                if 1 == len(w) and stringp(w[0]): w = w[0]

            if stringp(w):
                my.AddF(text(my, wx.ID_ANY, w), f)
            elif tuplep(w) and 2 == len(w) and intp(w[0]) and intp(w[1]):
                my.AddF(wx.Size(w[0], w[1]), wx.SizerFlags())
            elif tuplep(w):
                (call, args, kwargs) = w[0], list(w[1:]), {}
                if len(args) and hashp(args[-1]):
                    args, kwargs = args[:-1], args[-1]
                
                my.AddF(apply(call, [my,] + args, kwargs), f)
            else:
                w.Reparent(my)
                my.AddF(w, f) 

    def kids(my, match=None):
        if match:
            return [(c.GetName(), c) for c in my.Children
                    if match in c.GetName()]
        return [(c.GetName(), c) for c in my.Children]

    def theme(my, theme):
        my.SetBackgroundColour(theme["bg"])
        my.SetForegroundColour(theme["fg"])

    def find(my, key, branch=None):
        if stringp(key): key = key.split("/")
        if not branch: branch = my
        if not key: return branch

        which, key = key[0], key[1:]
        if "" == which:
            pass # Nothing to do.
        elif which.isdigit():
            try: branch = branch.Children[int(which)]
            except IndexError: raise KeyError, which
        else:
            for kid in branch.Children:
                if which == kid.GetName(): branch = kid; break
            else: raise KeyError, which

        return my.find(key, branch)

    def __getitem__(my, key): return my.find(key)

# h3. Class panel

class panel(wx.Panel):
    def __init__(my, *args, **kwargs):
        bg = settings.program.theme.bg
        fg = settings.program.theme.fg
        
        filtered = {}
        for key in kwargs.keys():
            if "BackgroundColour" == key: bg = kwargs["BackgroundColour"]
            elif "ForegroundColour" == key: fg = kwargs["ForegroundColour"]
            else: filtered[key] = kwargs[key]

        apply(wx.Panel.__init__, (my,) + args, filtered)
        
        my.SetBackgroundColour(bg)
        my.SetForegroundColour(fg)

class image(wx.StaticBitmap):
    def __init__(my, *args, **kwargs):
        file = None
        
        filtered = {}
        for key in kwargs.keys():
            if "file" == key: file = kwargs["file"]
            else: filtered[key] = kwargs[key]

        apply(wx.StaticBitmap.__init__, (my,) + args, filtered)
        
        image = wx.Image(file(), wx.BITMAP_TYPE_ANY)
        my.SetBitmap(wx.BitmapFromImage(image))

# h3. Class text

class text(wx.StaticText):
    def __init__(my, *args, **kwargs):
        bg = settings.program.theme.bg
        fg = settings.program.theme.fg
        size = None
        
        filtered = {}
        for key in kwargs.keys():
            if "BackgroundColour" == key: bg = kwargs["BackgroundColour"]
            elif "ForegroundColour" == key: fg = kwargs["ForegroundColour"]
            elif "size" == key: size = kwargs["size"]
            else: filtered[key] = kwargs[key]

        apply(wx.StaticText.__init__, (my,) + args, filtered)

        if "staticText" == my.GetName(): my.SetName(my.GetLabel()) # XXX Experimental.
        
        my.SetBackgroundColour(bg)
        my.SetForegroundColour(fg)
        if size: my.SetSize(size)

    def theme(my, theme):
        my.SetBackgroundColour(theme["bg"])
        my.SetForegroundColour(theme["fg"])

    def GetValue(my):
        return my.GetLabel()

# h3. Class heading

class heading(sizerpanel):
    def __init__(my, parent, id=-1, label=None, stripe=None, *args, **kwargs):
        apply(sizerpanel.__init__, (my, parent, id) + args, kwargs)

        if not stripe: stripe = "#000000"

        if not label: label = "Unlabelled"
        my._label = text(my, wx.ID_ANY, label)
        my._label.SetFont(wx.Font(
                14, wx.SWISS, wx.NORMAL, wx.NORMAL, False, "Tahoma"))
        line = wx.BoxSizer(wx.HORIZONTAL)

        my._stripe = wx.Panel(my, wx.ID_ANY, size=(5, 3))
        my._stripe.SetBackgroundColour(stripe)

        my.AddMany([
                (my._label, 0, wx.EXPAND | wx.ALL, 10),
                (my._stripe, 1, wx.EXPAND),
                ])

    def SetColour(my, colour):
        my._stripe.SetBackgroundColour(colour)
        my._stripe.Refresh()

    def SetLabel(my, text, stripe=None):
        my._label.SetLabel(text)
        if stripe: my.SetColour(stripe)
        my.Layout()

# h3. Class btn

class btn(wbuttons.GenButton):
    def __init__(my, *args, **kwargs):
        bg = settings.program.theme.bg
        fg = settings.program.theme.fg
        action = None
        
        filtered = {}
        for key in kwargs.keys():
            if "BackgroundColour" == key: bg = kwargs["BackgroundColour"]
            elif "ForegroundColour" == key: fg = kwargs["ForegroundColour"]
            elif "action" == key: action = kwargs["action"]
            else: filtered[key] = kwargs[key]

        apply(wbuttons.GenButton.__init__, (my,) + args, filtered)

        my.SetBezelWidth(1)
        my.SetBackgroundColour(bg)
        my.SetForegroundColour(fg)

        if "genbutton" == my.GetName(): my.SetName(my.GetLabel())

        my.Bind(wx.EVT_KEY_DOWN,         my.OnKeyDown)
        my.Bind(wx.EVT_KEY_UP,           my.OnKeyUp)

        if action: my.Bind(wx.EVT_BUTTON, action)


    def theme(my, theme):
        my.SetBackgroundColour(theme["bg"])
        my.SetForegroundColour(theme["fg"])

# The space-to-activate behaviour of the wx %buttons.py library is
# very annoying and upsetting to the user, since it can activate a
# button when they think they're doing something else (not least
# because space should play the QuickTime...). For now, I'm turning
# this completely off.

    def OnKeyDown(my, event): 
        event.Skip() # Will continue to process other events, eg. Shortcuts.

    def OnKeyUp(my, event): 
        event.Skip() # Will continue to process other events, eg. Shortcuts.

class pbtn(buttons.picture):
    def __init__(my, *args, **kwargs):
        apply(buttons.picture.__init__, (my,) + args, kwargs)

        my.SetBackgroundColour(settings.program.theme.bg)
        my.SetForegroundColour(settings.program.theme.fg)

        my.Bind(wx.EVT_KEY_DOWN,         my.OnKeyDown)
        my.Bind(wx.EVT_KEY_UP,           my.OnKeyUp)

    def theme(my, theme):
        my.SetBackgroundColour(theme["bg"])
        my.SetForegroundColour(theme["fg"])
        
    def OnKeyDown(self, event): event.Skip()

    def OnKeyUp(self, event): event.Skip()

# h3. Class tgl

class tgl(wbuttons.GenToggleButton):
    def __init__(my, *args, **kwargs):
        bg = settings.program.theme.bg
        fg = settings.program.theme.fg
        my.AltColour = (255, 255, 0)
        action = None
        
        filtered = {}
        for key in kwargs.keys():
            if "BackgroundColour" == key: bg = kwargs["BackgroundColour"]
            elif "ForegroundColour" == key: fg = kwargs["ForegroundColour"]
            elif "altcolour" == key: my.AltColour = kwargs["altcolour"]
            elif "action" == key: action = kwargs["action"]
            else: filtered[key] = kwargs[key]

        apply(wbuttons.GenToggleButton.__init__, (my,) + args, filtered)

        my.SetBezelWidth(1)
        my.SetBackgroundColour(bg)
        my.SetForegroundColour(fg)

        if action: my.Bind(wx.EVT_BUTTON, action)
        
    def theme(my, theme):
        my.SetBackgroundColour(theme["bg"])
        my.SetForegroundColour(theme["fg"])
        
    def OnKeyDown(self, event): event.Skip()

    def OnKeyUp(self, event): event.Skip()

class choice(wx.Choice):
    def __init__(my, *args, **kwargs):
        apply(wx.Choice.__init__, (my,) + args, kwargs)
        my.SetBackgroundColour(settings.program.theme.bg)
        my.SetForegroundColour(settings.program.theme.fg)

    def theme(my, theme):
        my.SetBackgroundColour(theme["bg"])
        my.SetForegroundColour(theme["fg"])

    def GetValue(my): return my.GetStringSelection()

class field(wx.TextCtrl):
    def __init__(my, *args, **kwargs):
        bg, fg = settings.program.theme.bg, settings.program.theme.fg

        filtered = {}
        for key in kwargs.keys():
            if "BackgroundColour" == key: bg = kwargs["BackgroundColour"]
            elif "ForegroundColour" == key: fg = kwargs["ForegroundColour"]
            else: filtered[key] = kwargs[key]
        
        apply(wx.TextCtrl.__init__, (my,) + args, filtered)
        
        my.SetBackgroundColour(bg); my.SetForegroundColour(fg)

    def theme(my, theme):
        my.SetBackgroundColour(theme["field_bg"])
        my.SetForegroundColour(theme["field_fg"])

class area(field):
    def __init__(my, *args, **kwargs):
        if "style" not in kwargs.keys(): kwargs["style"] = wx.TE_MULTILINE
        apply(field.__init__, (my,) + args, kwargs)
        
        my.SetFont(wx.Font(8, wx.MODERN, wx.NORMAL, wx.NORMAL, False,
                                   "Bitstream Vera Sans Mono"))

        if "text" == my.GetName(): my.SetName(my.GetValue())


class cb(wx.CheckBox):
    def __init__(my, *args, **kwargs):
        apply(wx.CheckBox.__init__, (my,) + args, kwargs)
        my.SetBackgroundColour(settings.program.theme.bg)
        my.SetForegroundColour(settings.program.theme.fg)

    def theme(my, theme):
        my.SetBackgroundColour(theme["bg"])
        my.SetForegroundColour(theme["fg"])

class rb(wx.RadioButton):
    def __init__(my, *args, **kwargs):
        apply(wx.RadioButton.__init__, (my,) + args, kwargs)
        my.SetBackgroundColour(settings.program.theme.bg)
        my.SetForegroundColour(settings.program.theme.fg)

    def theme(my, theme):
        my.SetBackgroundColour(theme["bg"])
        my.SetForegroundColour(theme["fg"])

class stacklabel(sizerpanel):
    def __init__(my, parent, label=None, gadget=None):
        sizerpanel.__init__(
            my, parent, wx.ID_ANY, widgets=(
                ((wx.SizerFlags().Left(), label),
                 gadget))
            )

# h2. Drop Target Classes

class dropper(wx.TextDropTarget):
    def __init__(my, o):
        wx.TextDropTarget.__init__(my)
        my._o = o

    def OnDropText(my, x, y, data): my._o.dropping(data)


class dropfile(wx.FileDropTarget):
    def __init__(my, o):
        wx.FileDropTarget.__init__(my)
        my._o = o

    def OnDragOver(my, x, y, count): return my._o.dropdrag(count)

    def OnLeave(my): my._o.dropleave()

    def OnDropFiles(my, x, y, filenames): my._o.dropping(filenames)


# I have really come to like the "facet menus" that some web sites use
# now. The idea is that you display all the choices laid out in a
# reasonably organized format so that the user can pick what they want
# without scrolling through five million line list as with a
# traditional drop-down select. 

# I use a wx.MiniFrame so that an entry won't be made in the Task Bar.

class spread(wx.MiniFrame):
    def __init__(my, parent, id, items, colours=None):
        wx.MiniFrame.__init__(my, parent, id, style=wx.NO_BORDER)

        my.SetSize((1280, 1024))
        my.SetVirtualSize((1280, 1024))

        my._scheme = Struct()
        my._scheme.bg = my.GetParent().GetBackgroundColour()
        my._scheme.border = my.GetParent().GetForegroundColour()
        my._scheme.text = my.GetParent().GetForegroundColour()
        my._scheme.font = wx.Font(
            8, wx.SWISS, wx.NORMAL, wx.NORMAL, False, "Tahoma")
        
        my._items = [[i, wx.Rect(), my._scheme.text] for i in items]
        if colours:
            for i in range(len(my._items)):
                my._items[i][-1] = colours[i]
        my._selected = None
        my._value = None

# To prevent nasty flicker, I use double-buffering. This requires that
# I create and initialize the RAM buffer (as you see here). The
# @OnPaint()@ handler will blit it into the real device context as needed.

        my._buffer = wx.EmptyBitmap(1280, 1024)
        dc = wx.BufferedDC(None, my._buffer)
        my.draw(dc)

        my.Bind(wx.EVT_PAINT, my.OnPaint)
        my.Bind(wx.EVT_MOTION, my.OnMotion)
        my.Bind(wx.EVT_LEFT_UP, my.OnLeftUp)
        my.Bind(wx.EVT_LEAVE_WINDOW, my.OnLeave)

    def OnLeave(my, event=None):
        my.Close()

    def OnPaint(my, event):
        dc = wx.BufferedPaintDC(my, my._buffer, wx.BUFFER_VIRTUAL_AREA)

    def draw(my, dc):
        dc.BeginDrawing()

        width = 64
        dc.SetFont(my._scheme.font)
        for (name, area, colour) in my._items: 
            width = max(width, dc.GetTextExtent(name)[0])
            
        # Background
        dc.SetBackground(wx.Brush(my._scheme.bg))
        dc.Clear()

        # Text
        sizes = [
            # Items, Columns, Sections
            (16,     1,       0),
            (50,     3,       5),
            (75,     4,       8),
            (100,    5,       10),
            (200,    7,       15),
            (300,    9,       20),
            (1000,   10,      30), # All bets are off.
            ]

        nitems = len(my._items)
        for limit, columns, sections in sizes: 
            if nitems < limit: break

        breaks = breaklist([x[0] for x in my._items], sections)
        
        cut = int((nitems + 1) / columns)
        origin = wx.Point(5, 5)
        coverage = wx.Rect(0, 0, 0, 0)

        x = origin.x; y = origin.y
        line = 0; tab = 0
        for i in range(len(my._items)):
            name, area, fg = my._items[i]
            (w, h) = dc.GetTextExtent(name)
            w += 30 # Apply padding.

            area.SetX(x)
            area.SetY(y)
            area.SetWidth(w)
            area.SetHeight(h)

            bg = my._scheme.bg
            if i == my._selected: bg, fg = fg, bg # Swap if hovering.

            dc.SetPen(wx.Pen(my._scheme.border, 0, wx.TRANSPARENT))
            dc.SetBrush(wx.Brush(bg))
            dc.DrawRectangle(area.x, area.y, area.width, area.height)

            dc.SetTextForeground(fg)
            dc.DrawText(name, x, y)

            coverage += area
            tab = max(tab, w)
            line += 1; y += h
            if name in breaks: y += h # Skip on section break.
            if line > cut:
                x += tab; tab = 0
                y = origin.y; line = 0

        my.SetSize((coverage.width + 5, coverage.height + 5))

        # Draw border.
        area = my.GetRect()
        dc.SetBrush(wx.Brush(my._scheme.bg, wx.TRANSPARENT))
        dc.SetPen(wx.Pen(my._scheme.border, 1))
        dc.DrawRectangle(area.x, area.y, area.width, area.height)

        dc.EndDrawing()

    def OnLeftUp(my, event):
        my.Close()
        if my._selected != None: my.GetParent().picking(my._selected)

    def OnMotion(my, event):
        where = event.GetPosition()

# Is it still over the same thing? If so, bail.

        if my._selected != None and \
                my._items[my._selected][1].Contains(where): 
            return

        found = None
        for i in range(len(my._items)):
            name, area, colour = my._items[i]
            if area.Contains(where): found = i; break

# If the cursor is outside everything, then I'll have a None. If
# nothing was selected before, then no update is needed.

        if my._selected == found: return

# Otherwise, I indicate the new selection with the @_selected@ property
# and request a repaint.

        dc = wx.BufferedDC(None, my._buffer)
        dc.SetFont(my._scheme.font)

        bg = my._scheme.bg # ; fg = my._scheme.text
        if my._selected != None:
            name, area, fg = my._items[my._selected]

            dc.SetPen(wx.Pen(my._scheme.border, 0, wx.TRANSPARENT))
            dc.SetBrush(wx.Brush(bg))
            dc.DrawRectangle(area.x, area.y, area.width, area.height)
            dc.SetTextForeground(fg)
            dc.DrawText(name, area.x, area.y)

            my.RefreshRect(area)

        my._selected = found
        if found != None:
            name, area, fg = my._items[my._selected]
            bg, fg = fg, bg

            dc.SetPen(wx.Pen(my._scheme.border, 0, wx.TRANSPARENT))
            dc.SetBrush(wx.Brush(bg))
            dc.DrawRectangle(area.x, area.y, area.width, area.height)
            dc.SetTextForeground(fg)
            dc.DrawText(name, area.x, area.y)

            my.RefreshRect(area)

    def OnDismiss(my, event=None):
        my.Close()

class readable(wx.StaticText):
    def __init__(my, *args, **kwargs):
        apply(wx.StaticText.__init__, (my,) + args, kwargs)

        my._font = wx.Font(
            14, wx.SWISS, wx.NORMAL, wx.NORMAL, False, "Tahoma")
        my.SetFont(my._font)
        my.SetBackgroundColour(settings.program.theme.bg)
        my.SetForegroundColour(settings.program.theme.fg)

    def theme(my, theme):
        my.SetBackgroundColour(theme["bg"])
        my.SetForegroundColour(theme["fg"])

class facet(readable):
    def __init__(my, parent, id, label=None, choices=None, colours=None, **kwargs):
        bg, fg = settings.program.theme.bg, settings.program.theme.fg
        size = None
        action = None

        filtered = {}
        for key in kwargs.keys():
            if    "BackgroundColour" == key: bg = kwargs["BackgroundColour"]
            elif  "ForegroundColour" == key: fg = kwargs["ForegroundColour"]
            elif  "action" == key: action = kwargs["action"]
            elif  "size" == key: size = kwargs["size"]
            else: filtered[key] = kwargs[key]

        if not label: label = "" # Because label is optional.
        apply(readable.__init__, (my, parent, id, label), filtered)

        my._colours = colours
        my._facet = None
        my._chosen = None
        my._items = []
        if choices: my.AppendItems(choices)

        my.SetBackgroundColour(bg); my.SetForegroundColour(fg)
        if size: my.SetSize(size)

        my.Bind(wx.EVT_LEFT_DOWN, my.on_click)
        if action: my.Bind(wx.EVT_CHOICE, action)

    def on_click(my, event):
        if my.IsEmpty(): return

        my._facet = spread(my, wx.ID_ANY, my.Items, my._colours)

        # Show popup below button.
        btn = event.GetEventObject()
        pos = btn.ClientToScreen((0,0))
        my._facet.SetPosition((pos.x, pos.y))

        my._facet.Show()

    def picking(my, choice):
        my.SetSelection(choice)
        
        e = wx.CommandEvent(wx.wxEVT_COMMAND_CHOICE_SELECTED, my.GetId())
        e.SetEventObject(my)
        my.GetEventHandler().ProcessEvent(e)

    def __getattr__(my, name):
        if   "Count" == name: return my.GetCount()
        elif "Items" == name: return my.GetItems()
        elif "Selection" == name: return my.GetSelection()
        elif "Strings" == name: return my.GetStrings()
        elif "StringSelection" == name: return my.GetStringSelection()
        else: return my.__dict__[name]

    def __setattr__(my, name, value):
        if   "Items" == name: return my.SetItems(value)
        elif "Selection" == name: return my.SetSelection(value)
        elif "StringSelection" == name: return my.SetStringSelection(value)
        else: my.__dict__[name] = value

    def Append(my, item, clientData):
        my._items.append(item)

    def AppendItems(my, x): 
        my._items += x

    def Clear(my): 
        my._items = []

    def Delete(my, n):
        del(my._items[n])

    def FindString(my, s):
        try: return my._items.index(s)
        except ValueError: return wx.NOT_FOUND

    def GetClientData(my, n):
        return None # Dummy.

    def GetCount(my):
        return len(my._items)

    def GetItems(my):
        return my._items

    def GetSelection(my):
        if my._chosen != None: return my._chosen
        return wx.NOT_FOUND

    def GetString(my, n):
        return my._items[n]

    def GetStrings(my):
        return my._items

    def GetStringSelection(my):
        if None == my._chosen or my._chosen < 0: return ""
        return my._items[my._chosen]
    GetValue = GetStringSelection

    def Insert(my, item, pos=0, clientData=None):
        my._items.insert(pos, item)

    def IsEmpty(my): 
        return 0 == len(my._items)

    def Select(my, n): return my.SetSelection(n)

    def SetClientData(my, n, clientData):
        pass # Dummy.

    def SetItems(my, items, colours=None):
        my._items = items
        my._colours = colours
        my.SetSelection(0)
    
    def SetLabel(my, text):
        s = my.GetSize()
        readable.SetLabel(my, text)
        my.SetSize(s)

    def SetSelection(my, n): 
        my._chosen = n

        if my.IsEmpty() or None == n or n < 0: 
            my.SetLabel("???")
        else: my.SetLabel(my._items[n])

        # my.GetParent().Fit()
        # my.GetParent().Layout()
        my.Refresh()

    def SetString(my, n, s):
        my._items[n] = s

    def SetStringSelection(my, s):
        my.SetSelection(my.FindString(s))

    def SetValue(my, s): my.SetStringSelection(s)

# h3. class filepick

class filepick(wx.Panel):
    def __init__(my, parent, id, caption, types, **kwargs):
        apply(wx.Panel.__init__, (my, parent, id), kwargs)

        my._caption = caption
        my._types = types
        my._widgets = Struct(); w = my._widgets

        x = wx.BoxSizer(wx.HORIZONTAL)
        x.AddF(text(my, -1, my._caption), wx.SizerFlags().Centre())
        w.path = field(my)
        x.AddF(w.path, wx.SizerFlags().Proportion(6).Border(wx.ALL, 5))
        w.select = btn(my, -1, "...", size=(32, -1))
        x.AddF(w.select, wx.SizerFlags().Border(wx.ALL, 5))
        w.explore = pbtn(my, -1, "folder")
        x.AddF(w.explore, wx.SizerFlags().Border(wx.ALL, 5))
        my.SetSizer(x)

        my.Layout()

        my.Bind(wx.EVT_BUTTON, my.selecting, w.select)
        my.Bind(wx.EVT_BUTTON, my.exploring, w.explore)

    def GetValue(my): return my._widgets.path.GetValue()

    def SetValue(my, path): my._widgets.path.SetValue(path); return my

    def selecting(my, event):
        "( my event --) Select file for path."

        d = wx.FileDialog(
            my, my._caption,
            defaultDir=os.path.dirname(
                paths.convert(my._widgets.path.GetValue())),
            defaultFile="",
            wildcard=my._types,
            style=wx.OPEN|wx.CHANGE_DIR
            )
        if d.ShowModal() == wx.ID_OK:
            my._widgets.path.SetValue(d.GetPaths()[0])
            my.picked()
        d.Destroy()

    def exploring(my, event):
        "( my event --) Open up Windows Explorer into path directory."

        where = paths.convert(os.path.dirname(my._widgets.path.GetValue()))
        if os.path.exists(where): os.startfile(where)

    def picked(my):
        e = wx.CommandEvent(wx.wxEVT_COMMAND_CHOICE_SELECTED, my.GetId())
        e.SetEventObject(my)
        my.GetEventHandler().ProcessEvent(e)

# h3. class dirpick

class dirpick(filepick):
    def selecting(my, event):
        "( my event --) Select directory."

        start = my._widgets.path.GetValue()
        if not len(start): start = my._types # XXX Silly overload.
        d = wx.DirDialog(
            my, my._caption,
            defaultPath=paths.convert(start),
            style=wx.DD_DEFAULT_STYLE)
        if d.ShowModal() == wx.ID_OK: my._widgets.path.SetValue(d.GetPath())
        d.Destroy()

# h2. Sort Images

# The wretched buckets of vomitus below are %.png encoded images for
# use by the sort columns of the list control. They should be replaced
# by actual image files as soon as practicable.

def getSmallUpArrowData():
    return \
'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\
\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\
\x00\x00<IDAT8\x8dcddbf\xa0\x040Q\xa4{h\x18\xf0\xff\xdf\xdf\xffd\x1b\x00\xd3\
\x8c\xcf\x10\x9c\x06\xa0k\xc2e\x08m\xc2\x00\x97m\xd8\xc41\x0c \x14h\xe8\xf2\
\x8c\xa3)q\x10\x18\x00\x00R\xd8#\xec\xb2\xcd\xc1Y\x00\x00\x00\x00IEND\xaeB`\
\x82' 

def getSmallUpArrowImage():
    stream = cStringIO.StringIO(getSmallUpArrowData())
    return wx.ImageFromStream(stream)

def getSmallUpArrowBitmap():
    return wx.BitmapFromImage(getSmallUpArrowImage())

def getSmallDnArrowData():
    return \
"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\
\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\
\x00\x00HIDAT8\x8dcddbf\xa0\x040Q\xa4{\xd4\x00\x06\x06\x06\x06\x06\x16t\x81\
\xff\xff\xfe\xfe'\xa4\x89\x91\x89\x99\x11\xa7\x0b\x90%\ti\xc6j\x00>C\xb0\x89\
\xd3.\x10\xd1m\xc3\xe5*\xbc.\x80i\xc2\x17.\x8c\xa3y\x81\x01\x00\xa1\x0e\x04e\
?\x84B\xef\x00\x00\x00\x00IEND\xaeB`\x82" 

def getSmallDnArrowImage():
    stream = cStringIO.StringIO(getSmallDnArrowData())
    return wx.ImageFromStream(stream)

def getSmallDnArrowBitmap():
    return wx.BitmapFromImage(getSmallDnArrowImage())

class reporter(wx.ListCtrl, listmix.ListCtrlAutoWidthMixin):
    def __init__(my, parent, ID, pos=wx.DefaultPosition,
                 size=wx.DefaultSize, style=0):
        wx.ListCtrl.__init__(my, parent, ID, pos, size, style)
        listmix.ListCtrlAutoWidthMixin.__init__(my)

    def GetSelected(my):
        found = []

        i = my.GetFirstSelected()
        while i != -1: 
            found.append(i)
            i = my.GetNextSelected(i)

        return found

# h2. Score Display

class score(text):
    def __init__(my, *args, **kwargs):
        apply(text.__init__, (my,) + args, kwargs)

        my.SetFont(wx.Font(28, wx.MODERN, wx.NORMAL, wx.NORMAL, False,
                           "Bitstream Vera Sans Mono"))
